// Copyright 2010 Google Inc. All Rights Reserved.

/**
 * @fileoverview OAuth2 client.  Only contains those methods necessary for
 * processing OAuth2 requests.  All UI components are in auth-ui.js.
 * See http://goto/oauth2.
 *
 * @author monsur@google.com (Monsur Hossain)
 */

goog.provide('googleapis.auth');


/**
 * Query parameter keys for doing OAuth2.
 * @enum {string}
 */
googleapis.auth.OAuth2Keys = {

  // OAuth2's key for the client ID.
  CLIENT_ID: 'client_id',

  // OAuth2's key for the redirect URI.
  REDIRECT_URI: 'redirect_uri',

  // OAuth2's key for the response type.
  RESPONSE_TYPE: 'response_type',

  // OAuth2's key for the state, who's value is any user-specified string.
  STATE: 'state'
};


/**
 * Key for the authorize URI.
 * @type {string}
 * @private
 */
googleapis.auth.AUTHORIZE_URI_KEY_ = 'authorize_uri';


/**
 * Prefix for any cookies set by this code.
 * @type {string}
 * @private
 */
googleapis.auth.COOKIE_PREFIX_ = 'oauth2-';


/**
 * The key used to identify a token (if none is provided by the user).
 * @type {string}
 * @private
 */
googleapis.auth.TOKEN_KEY_ = 'token';


/**
 * The default separator to use when creating and parsing key/value strings.
 * @type {string}
 * @private
 */
googleapis.auth.DEFAULT_SEPARATOR_ = '&';


/**
 * The default expiration time for the token.
 * @type {number}
 * @private
 */
googleapis.auth.DEFAULT_EXPIRES_ = 3600;


/**
 * Google's OAuth2 authorization endpoint.
 * Note that this will change upon launch.
 * @type {string}
 * @private
 */
//TODO(monsur): This location will change before the official launch.
googleapis.auth.GOOGLE_AUTHORIZE_URI_ =
    'https://www.google.com/accounts/o8/oauth2/authorization';


/**
 * Stores any temporary parameters related to processing the OAuth2 request,
 * including a pointer to the auth window or iframe, and the user's callback to
 * call after authorization.
 * @type {?Object}
 * @private
 */
googleapis.auth.currentContext_ = null;


/**
 * Key/Value pair of tokens assigned to this application.
 * @type {Object}
 * @private
 */
googleapis.auth.tokens_ = {};


/**
 * Indicates whether the token cookie(s) has been loaded already.
 * @type {boolean}
 * @private
 */
googleapis.auth.loaded_ = false;


/**
 * Indicates whether the token values need to be written to the cookie.
 * {@code true} indicates the values have changed and need to be written to the
 * cookie; {@code false} indicates no changes have been made.
 * @type {boolean}
 * @private
 */
googleapis.auth.tokensDirty_ = false;


/**
 * Creates a key/value pair string separated by an '='.  The key and value are
 * both URI encoded.
 * @param {string} key The key.
 * @param {string} value The value.
 * @return {string} A string representation of the key/value pair, separated by
 *     an '='.  Returns an empty string if the value is empty.
 * @private
 */
googleapis.auth.getKeyValuePair_ = function(key, value) {
  if (value != null) {
    return encodeURIComponent(key) + '=' + encodeURIComponent(value);
  }
  return '';
};


/**
 * Serializes a key/value object to a string.
 * @param {Object} obj The object to be serialized.
 * @param {string=} opt_separator1 The separator to use between key/value pairs.
 *     Defaults to '&'.
 * @return A serialized string of the key/value pair.  For example, the object
 *     {"a": 1, "b": 2} would return "a=1&b=2" (using the default separator).
 * @private
 */
googleapis.auth.serializeKeyValuePair_ = function(obj, opt_separator1) {
  var str = '';
  if (!obj) {
    return str;
  }
  var separator1 = opt_separator1 || googleapis.auth.DEFAULT_SEPARATOR_;
  for (var name in obj) {
    if (!obj.hasOwnProperty(name)) {
      continue;
    }
    var pair = googleapis.auth.getKeyValuePair_(name, obj[name]);
    if (!pair) {
      continue;
    }
    if (str) {
      str += separator1;
    }
    str += pair;
  }
  return str;
};


/**
 * Parses a key/value pair string into an object.
 * @param {string} value The string value to parse.
 * @param {string=} opt_separator1 The separator to use between key/value pairs.
 *     Defaults to '&'.
 * @return An object representation of the key/value pairs.  For example, the
 *     string "a=1&b=2" would return {"a": 1, "b": 2} (using the default
 *     separator).
 * @private
 */
googleapis.auth.parseKeyValuePair_ = function(value, opt_separator1) {
  var obj = {};
  if (!value) {
      return obj;
  }
  var separator1 = opt_separator1 || googleapis.auth.DEFAULT_SEPARATOR_;
  var params = value.split(separator1);
  for (var i = 0; i < params.length; i++) {
    var param = params[i];
    if (!param) {
      continue;
    }
    var pair = param.split('=');
    obj[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
  }
  return obj;
};


/**
 * Loads the token(s) from the cookie (if it hasn't been loaded already).
 * @private
 */
googleapis.auth.load_ = function() {
  if (googleapis.auth.loaded_) {
    return;
  }
  var tokens = {};
  var alltokens = googleapis.auth.parseKeyValuePair_(document.cookie, ';');
  for (var name in alltokens) {
    if (!alltokens.hasOwnProperty(name) ||
        name.indexOf(googleapis.auth.COOKIE_PREFIX_) != 0) {
      continue;
    }
    // Only load cookies related to this application.
    tokens[name] = googleapis.auth.parseKeyValuePair_(alltokens[name]);
  }
  googleapis.auth.tokens_ = tokens;
  googleapis.auth.loaded_ = true;
};


/**
 * Retrieves the string used to set a single cookie.
 * @param {string} name The name of the cookie.
 * @param {string} value The value of the cookie.
 * @param {number} expires The cookie expiration in seconds.
 * @return {string} The cookie string.
 * @private
 */
googleapis.auth.getCookieStr_ = function(name, value, expires) {
  var date = new Date();
  date.setTime(date.getTime() + (expires * 1000));
  var obj = {};
  obj[name] = value;
  obj['expires'] = date.toUTCString();
  return googleapis.auth.serializeKeyValuePair_(obj, '; ');
};


/**
 * Writes the token(s) to the cookie.
 * @private
 */
googleapis.auth.write_ = function() {
  if (!googleapis.auth.tokensDirty_) {
    return;
  }
  for (var name in googleapis.auth.tokens_) {
    if (!googleapis.auth.tokens_.hasOwnProperty(name)) {
      // Skip cookies that don't belong to this application.
      continue;
    }
    var value = '';
    // Default to a date in the past to delete the cookie.  The actual interval
    // doesn't matter, as long as its in the past.  The default below sets the
    // cookie to 1 hour in the past.
    var expires = -3600;
    var token = googleapis.auth.tokens_[name];
    if (token) {
      // If token exists, write it, otherwise delete the cookie.
      expires = token['expires_in'] || googleapis.auth.DEFAULT_EXPIRES_;
      value = googleapis.auth.serializeKeyValuePair_(token);
    }
    document.cookie = googleapis.auth.getCookieStr_(name, value, expires);
  }
  googleapis.auth.tokensDirty_ = false;
};


/**
 * Retrieves the full key used to set the cookie.
 * @param {string} key The local key.
 * @return {string} The full key, which is the local key plus the cookie prefix.
 * @private
 */
googleapis.auth.getKey_ = function(key) {
  return googleapis.auth.COOKIE_PREFIX_ + key;
};


/**
 * Retrieves the token from the cookie.
 * @param {string=} opt_key The key to load.
 * @return {?Object} Returns the token matching the key (if a key is provided),
 *     or the first token (if no key is provided), or null (if the token is not
 *     found).
 */
googleapis.auth.getToken = function(opt_key) {
  googleapis.auth.load_();
  if (opt_key) {
    return googleapis.auth.tokens_[googleapis.auth.getKey_(opt_key)] || null;
  } else {
    // If no key is specified, return any token.  The assumption here is that
    // there is only one token in the token store, or the user doesn't care
    // which token is retrieved.  This is not idempotent, and depends on the
    // order returned by the for/in statement. (although in practice, it will
    // probably be idempotent).
    // TODO(monsur): Be more rigorous about which token is returned in this
    // case?  It is only relevant with there is more than one token in the
    // token store.
    for (var name in googleapis.auth.tokens_) {
      if (!googleapis.auth.tokens_.hasOwnProperty(name)) {
        continue;
      }
      return googleapis.auth.tokens_[name] || null;
    }
  }
};


/**
 * Sets the token to the given value.
 * @param {string} key The token key.
 * @param {Object} token The token.
 * @param {boolean=} opt_write Whether to write the token to the cookie.
 *     Defaults to true.
 */
googleapis.auth.setToken = function(key, token, opt_write) {
  var write = opt_write || true;
  googleapis.auth.load_();
  googleapis.auth.tokens_[googleapis.auth.getKey_(key)] = token;
  googleapis.auth.tokensDirty_ = true;
  if (write) {
    googleapis.auth.write_();
  }
};


/**
 * Removes the token.
 * @param {string} key The token key.
 * @param {boolean=} opt_write Whether to write the token to the cookie.
 *     Defaults to true.
 */
googleapis.auth.removeToken = function(key, opt_write) {
  googleapis.auth.setToken(key, null, opt_write);
};


/**
 * Removes all tokens and clears the auth cookie.
 * @private
 */
googleapis.auth.clearAll_ = function() {
  googleapis.auth.load_();
  for (var name in googleapis.auth.tokens_) {
    if (!googleapis.auth.tokens_.hasOwnProperty(name)) {
      continue;
    }
    googleapis.auth.tokens_[name] = null;
  }
  googleapis.auth.write_();
};


/**
 * @return {string} The URI to redirect to after the user authorizes.
 * @param {string=} opt_uri An optional URI to use as the redirect URI.
 * @private
 */
googleapis.auth.getRedirectUri_ = function(opt_uri) {
  var uri = opt_uri || window.location.href;
  return uri.replace(/[#][\s\S]*/, '');
};


/**
 * @param {string} name The parameter to check.
 * @return {boolean} Indicates whether the given parameter is reserved for
 *     OAuth2.
 * @private
 */
googleapis.auth.isReservedParameter_ = function(name) {
  for (var key in googleapis.auth.OAuth2Keys) {
    if (googleapis.auth.OAuth2Keys.hasOwnProperty(key) &&
        googleapis.auth.OAuth2Keys[key] == name) {
      return true;
    }
  }
  return false;
};


/**
 * Gets the authorize URI for this request.
 * @param {?Object} params OAuth2-related auth params.
 * @return {string} The OAuth2 authorize URI with the appropriate query
 *     parameters.
 */
googleapis.auth.getAuthorizeUri = function(params) {
  params = params || {};

  // Get the authorize base URI.
  var authUri = params[googleapis.auth.AUTHORIZE_URI_KEY_] ||
      googleapis.auth.GOOGLE_AUTHORIZE_URI_;
  delete params[googleapis.auth.AUTHORIZE_URI_KEY_];

  // Figure out the OAuth2 query parameters.
  params[googleapis.auth.OAuth2Keys.CLIENT_ID] =
      params[googleapis.auth.OAuth2Keys.CLIENT_ID] || 'anonymous';
  params[googleapis.auth.OAuth2Keys.REDIRECT_URI] =
      params[googleapis.auth.OAuth2Keys.REDIRECT_URI] ||
          googleapis.auth.getRedirectUri_();
  params[googleapis.auth.OAuth2Keys.RESPONSE_TYPE] =
      params[googleapis.auth.OAuth2Keys.RESPONSE_TYPE] || 'token';

  // Add the query parameters to the URI.
  authUri += authUri.indexOf('?') > 0 ? '&' : '?';
  authUri += googleapis.auth.serializeKeyValuePair_(params);

  return authUri;
};


/**
 * @return {?Object} If the query parameter or fragment contains an OAuth2
 *     response, parses that response and returns it as an object.
 * @param {Location} opt_location A window.location object containing a search
 *     and/or hash property.
 */
googleapis.auth.getTokenFromFragment = function(opt_location) {
  // The hash and search properties in window.location are prefixed with a '?'or
  // '#', respectively (if they exist).  This helper function strips off that
  // prefix character are returns the rest of the string, or returns null if
  // there is no input string.
  var getUriSection = function(section) {
    return section ? section.substring(1) : null;
  };

  var location = opt_location || window.location;

  // Check for errors on the query string.
  var queryData = googleapis.auth.parseKeyValuePair_(
      getUriSection(location.search));
  if (queryData['error']) {
    return queryData;
  }

  // Check for a token on the URI fragment.
  var hashData = googleapis.auth.parseKeyValuePair_(
      getUriSection(location.hash));
  if (hashData['access_token']) {
    return hashData;
  }

  // No token or error.
  return null;
};


/**
 * Stores the variables required for a login request.
 * @param {Object} params The request parameters.
 * @param {Function=} opt_callback An option callback to call once auth is
 *     completed.
 * @return {string} The authorization URI.
 * @private
 */
googleapis.auth.prepareLogin_ = function(params, opt_callback) {
  params = params || {};
  googleapis.auth.currentContext_ = {};
  if ('key' in params) {
    googleapis.auth.currentContext_['key'] = params['key'];
    delete params['key'];
  }
  googleapis.auth.currentContext_['callback'] =
      opt_callback || function(resp) {};
  return googleapis.auth.getAuthorizeUri(params);
};


/**
 * Initiates a login request by opening a popup window with the authorization
 * page.
 * @param {Object} params The request parameters.
 * @param {Function=} opt_callback An option callback to call once auth is
 *     completed.
 */
googleapis.auth.login = function(params, opt_callback) {
  var authorizeUri = googleapis.auth.prepareLogin_(params, opt_callback);
  if (googleapis.auth.currentContext_['popup']) {
    // If the popup already exists but is hidden, show it.
    googleapis.auth.currentContext_['popup'].focus();
  } else {
    // TODO(monsur): This doesn't center the window in certain cases.  Figure
    // out which cases and fix.
    var width = 400;
    var height = 400;
    var left = (screen.width - width) / 2;
    var top = (screen.height - height) / 2;
    googleapis.auth.currentContext_['popup'] = window.open(authorizeUri,
        'OAuth2',
        'toolbar=no, location=no, directories=no, status=no, menubar=no, ' +
        'scrollbars=no, resizable=no, copyhistory=no, width=' + width +
        ', height=' + height + ', top=' + top + ', left=' + left);
  }
};


/**
 * Checks for an OAuth2 token in the URI fragment.  If it exists, kick off the
 * next step of the OAuth2 process.  The function should be called on every page
 * load, as early as possible.
 */
googleapis.auth.checkLoginComplete = function() {
  var token = googleapis.auth.getTokenFromFragment();
  if (token) {
    // TODO(monsur): RED FLAG!! Its very bad to be using window.opener or
    // window.parent here.  Instead, have the parent code set a property on the
    // child window that points to the callback function.  This callback
    // function should be preserved for the lifetime of the authorization
    // process.  Once the auth process returns to the same domain, the child
    // window should be able to call the callback.
    if (window.opener) {
      window.opener['googleapis']['auth']['receiveLoginComplete'](token);
    } else if (window.parent && window.parent != window) {
      window.parent['googleapis']['auth']['receiveLoginComplete'](token);
    }
  }
};


/**
 * Receives the OAuth2 token and completes the OAuth2 flow.
 * @param {Object} token The OAuth2 token.
 */
googleapis.auth.receiveLoginComplete = function(token) {
  token = token || {};

  // If a key was specified, use it to store the token.
  var key =
      googleapis.auth.currentContext_['key'] || googleapis.auth.TOKEN_KEY_;
  googleapis.auth.setToken(key, token);

  if (googleapis.auth.currentContext_['popup']) {
    // If there is a popup, close it.
    try {
      googleapis.auth.currentContext_['popup']['close']();
    } catch (e) {}
    googleapis.auth.currentContext_['popup'] = null;
  }

  if (googleapis.auth.currentContext_['iframe']) {
    // If there is a hidden iframe, remove it from the DOM.
    document.body.removeChild(googleapis.auth.currentContext_['iframe']);
    googleapis.auth.currentContext_['iframe'] = null;
  }

  if (googleapis.auth.currentContext_['callback']) {
    // If there is a callback method, call it.
    googleapis.auth.currentContext_['callback'](token);
    googleapis.auth.currentContext_['callback'] = null;
  }

  // Remove any state data.
  googleapis.auth.currentContext_ = null;
};


/**
 * Initiates an "immediate mode" login.  Immediate mode login doesn't have any
 * UI and returns the OAuth2 token immediately.  The flow takes place inside a
 * hidden iframe.
 * @param {Object} params The request parameters.
 * @param {Function=} opt_callback An option callback to call once auth is
 *     completed.
 */
googleapis.auth.loginImmediate = function(params, opt_callback) {
  params = params || {};
  params['immediate'] = 'yes';
  var authorizeUri = googleapis.auth.prepareLogin_(params, opt_callback);

  var frame = document.createElement('iframe');
  frame.src = authorizeUri;
  frame.style.width = '1px';
  frame.style.height = '1px';
  frame.style.position = 'absolute';
  frame.style.left = '-100px';
  document.body.appendChild(frame);

  googleapis.auth.currentContext_['iframe'] = frame;
};


/**
 * Deletes the user's OAuth2 token from the cookie, but does not revoke access.
 * @param {string=} opt_key An optional key parameter.  If specified, only the
 *     token related to this key will be deleted.
 */
googleapis.auth.logout = function(opt_key) {
  //TODO(monsur): Handle revoke access on logout?
  if (opt_key) {
    googleapis.auth.removeToken(opt_key);
  } else {
    googleapis.auth.clearAll_();
  }
};
